Release 10.1A: OpenEdge Development:
ProDataSets


Passing a ProDataSet parameter by reference

You can only specify BY-REFERENCE on an internal procedure or user-defined function call, not on a RUN of an external procedure. When you pass a ProDataSet parameter by reference, the called routine’s ProDataSet definition is bound to the calling routine’s ProDataSet only for the duration of the call.

To pass a ProDataSet by reference, in the calling routine, use the BY-REFERENCE keyword in the RUN statement that defines the ProDataSet parameter. There is no special syntax required in the called routine. However, since the calling routine’s ProDataSet instance is substituted for the called routine’s ProDataSet, only the definition of the ProDataSet is required by the called routine. In other words, the same ProDataSet is defined in both routines but only one is actually used. If the called routine’s ProDataSet is not directly used to hold its own data anywhere within the routine, you can save the overhead of allocating it by including the REFERENCE-ONLY keyword on its definition. This keyword tells Progress to use the definition for compiler references to the table and its fields but not to instantiate it at run time. Any reference to the ProDataSet, except where it is passed in from another routine BY-REFERENCE, results in a runtime error.

Because of the efficiency of passing the ProDataSet with BY-REFERENCE, you should normally use this keyword in your parameter definitions in RUN statements for any case where the called procedure will always or sometimes be in the same session as the caller. Because Progress ignores the keyword on a remote call, you get the most efficient behavior in either case.

Why then is BY-REFERENCE not the default behavior?

Passing objects by copying them is the standard for other Progress parameter types. Passing a ProDataSet by reference has certain side effects that you need to be aware of, which could make the behavior confusing if you are not conscious of what Progress is actually doing in the background to enable your called procedure to point to the same ProDataSet instance as the caller. For this reason, you have to make a specific request to pass by reference, and you should always make sure you have considered the consequences before doing so. Since this is different from how Progress behaves otherwise, some of the effects can seem counter-intuitive, so we explain them in some detail here.

In general you must consider that on a call BY-REFERENCE, Progress substitutes the ProDataSet handle in the calling procedure for the ProDataSet defined in the called procedure, and that is the cause of most of the side effects. Consider the cases described in the following sections.

INPUT BY-REFERENCE can be like INPUT-OUTPUT

In a local call, passing a ProDataSet as an INPUT parameter BY-REFERENCE makes it behave essentially like an INPUT-OUTPUT parameter. The called procedure uses its local definition of the ProDataSet, if any, only to verify that the definition is compatible with the INPUT parameter. It then adjusts any references to the parameter to point to the ProDataSet instance in the caller. As a result, any changes you make to data in the ProDataSet in the called procedure are actually made to the ProDataSet in the calling procedure, and so are visible there after the procedure returns.

Design tip: If the called procedure can make changes that should be visible in the caller, then you should make the parameter INPUT-OUTPUT. This reflects what’s actually going on, and also provides the correct behavior when the call is remote, as the changes must explicitly be passed back to the caller in that case. If the called procedure will only read the data and not change it, then make the parameter INPUT.

Why not always make the parameter INPUT-OUTPUT in this case?

If you configure your application so that the call is made to a remote session and the called procedure doesn’t make any changes to the ProDataSet, then Progress will needlessly pass the unchanged ProDataSet back to the caller, creating unnecessary network traffic. This is why the INPUT mode is still useful.

OUTPUT BY-REFERENCE can be like OUTPUT APPEND

If the ProDataSet parameter is OUTPUT BY-REFERENCE and the call is local, then any data added to the ProDataSet in the called procedure is effectively appended to what was already there, again because both procedures are actually pointing at the same ProDataSet instance. If you make the same call remotely or you don’t make it BY-REFERENCE, then the target ProDataSet in the calling procedure is emptied automatically by Progress before the data from the OUTPUT parameter is copied into it. This can give you different behavior between local and remote RUN statements.

Design tip: When you pass an OUTPUT parameter BY-REFERENCE, and don’t want APPEND behavior, always explicitly empty the ProDataSet in the calling procedure just before the call.

This might seem like extra overhead, but in fact if you execute the statement hDataSet:EMPTY-DATASET(), this executes exactly the same code internally as Progress uses to empty the ProDataSet for you, so the net cost is the same. If you do want APPEND behavior, then include the APPEND keyword on the parameter as you would for a temp-table.

ProDataSet instance passed BY-REFERENCE must exist in the caller

Consider this situation where a ProDataSet is passed as an OUTPUT parameter. The calling procedure defines only a handle for the ProDataSet and passes it using the DATASET-HANDLE parameter form, without creating a dynamic ProDataSet first:

Syntax
RUN called-procedure ( OUTPUT DATASET-HANDLE dataset-handle BY-REFERENCE ). 

The intention is that the called-procedure’s ProDataSet definition and data are passed back as the OUTPUT parameter to populate the dataset-handle in the caller. This cannot work properly when the call is local. When the call is made, the dataset-handle has the Unknown value (?) because it has not been used yet. But the BY-REFERENCE keyword instructs Progress to use the calling procedure’s ProDataSet as the basis for the parameter. Since there is no such ProDataSet at the time of the call, this results in an error. A ProDataSet passed BY-REFERENCE, regardless of the parameter mode, must be initialized by specifying its tables and relations before the call.

In this case the calling procedure must do one of the following:

If the OUTPUT parameter is a DATASET-HANDLE, it presumably means that the calling procedure is prepared to accept a variety of ProDataSet definitions returned to it. If this is the case, then once a particular ProDataSet has been passed back and has populated the dynamic ProDataSet, any further calls using the same RUN statement form must receive back a ProDataSet of the same type. Even though Progress empties the target dynamic ProDataSet so that the data from the called-procedure replaces any data in the calling procedure’s ProDataSet, Progress does not automatically delete the dynamic ProDataSet structure in the calling procedure. If it is not compatible with the OUTPUT parameter, an error results.

If you want to use the same dynamic ProDataSet handle to receive different ProDataSets during the lifetime of the calling procedure, you must delete the dynamic ProDataSet using the DELETE OBJECT statement. Set the handle variable to the Unknown value (?) before you run a called-procedure to get back a different ProDataSet to avoid the error.

Sometimes the calling procedure needs to get back a dynamic ProDataSet (that is, one with a variable definition) on the first call and then wants to be able to make further calls to get back new or additional data in the same ProDataSet type, but without copying the ProDataSet definition locally on subsequent calls. If this is the case, then the first RUN statement must be made by value, in order to get back the ProDataSet definition with the data, and then subsequent calls can be made with a separate RUN statement BY-REFERENCE. This avoids copying the ProDataSet definition locally. (Note that in these descriptions the words “by value” are normally not capitalized in order to emphasize that this is the default behavior, so that the keyword BY-VALUE is not required to get this behavior.)

Naturally you could not pass an uninitialized dynamic ProDataSet as an INPUT or INPUT-OUTPUT parameter, as there would be no definition to pass to the called-procedure. In such a case you could only pass the handle variable and let the called-procedure associate it with a ProDataSet of its own.

Design tip: To summarize: if the target ProDataSet in the calling procedure is dynamic, then you must either initialize it before making a call to another local procedure BY-REFERENCE, or make the first call by value and subsequent calls BY-REFERENCE so that you obtain the ProDataSet definition the caller needs.

Main block references ignored in internal procedures

When the called procedure is a persistent procedure, its ProDataSet definition will naturally be in the main block of the procedure, that is, outside of any internal procedure. A ProDataSet definition is in fact not even allowed in an internal procedure. However, if its internal procedures are ever passed a ProDataSet parameter BY-REFERENCE, it is important that you not reference the ProDataSet handle in any way in the main block if you expect the effect of that reference to be visible in the internal procedures. This case is insidious enough to merit a specific example and diagram.

Here’s a simple procedure that defines the dsOrder ProDataSet, runs another procedure persistent, and then runs an internal procedure to fill the ProDataSet:

/* refCaller.p */ 
{dsOrderTT.i} 
{dsOrder.i} 
DEFINE VARIABLE hProc AS HANDLE     NO-UNDO. 
RUN refCallee.p PERSISTENT SET hProc. 
RUN fillProc IN hProc (OUTPUT DATASET dsOrder ). 

Here’s the procedure it runs. It defines its own instance of the ProDataSet and then uses its handle to attach three Data-Sources. Inside the internal procedure fillProc, it fills the ProDataSet and returns it as an OUTPUT parameter, as shown:

 /* refCallee.p */ 
 {dsOrderTT.i} 
 {dsOrder.i} 
 DEFINE VARIABLE hDset AS HANDLE     NO-UNDO. 
 DEFINE DATA-SOURCE srcOrder FOR Customer. 
 DEFINE DATA-SOURCE srcOline FOR OrderLine. 
 DEFINE DATA-SOURCE srcItem FOR ITEM. 
     
    hDset = DATASET dsorder:HANDLE. 
    hDset:GET-BUFFER-HANDLE(1):ATTACH-DATA-SOURCE(DATA-SOURCE 
srcOrder:HANDLE). 
    hDset:GET-BUFFER-HANDLE(2):ATTACH-DATA-SOURCE(DATA-SOURCE 
srcOline:HANDLE). 
    hDset:GET-BUFFER-HANDLE(3):ATTACH-DATA-SOURCE(DATA-SOURCE 
srcItem:HANDLE). 
 PROCEDURE fillProc: 
    DEFINE OUTPUT PARAMETER DATASET FOR dsOrder. 
    DATASET dsOrder:FILL(). 
 END PROCEDURE. 

If you run the main procedure refCaller.p, you get the following error:

What happened? All three Data-Sources were attached in the main block, so why can’t Progress see them?

The reason is that the instance of dsOrder defined in the main block, the one whose handle was used to attach the Data-Sources, isn’t the one used by the internal procedure. Because the ProDataSet is passed in by reference, fillProc is pointing to the instance of dsOrder defined in refCaller.p, which has no Data-Sources. A few messages confirm this.

Let’s display the ProDataSet handle in the calling procedure:

 RUN refCallee.p PERSISTENT SET hProc. 
 MESSAGE "In the calling proc, dsOrder is " DATASET dsOrder:HANDLE 
        VIEW-AS ALERT-BOX. 
 RUN fillProc IN hProc (OUTPUT DATASET dsOrder BY-REFERENCE). 

Also, in the main block of the persistent procedure refCallee.p:

MESSAGE "In the main block, dsOrder is " DATASET dsOrder:HANDLE 
        VIEW-AS ALERT-BOX. 

And, in the internal procedure fillProc:

PROCEDURE fillProc: 
    DEFINE OUTPUT PARAMETER DATASET FOR dsOrder. 
    MESSAGE "In the fillProc, dsOrder is " DATASET dsOrder:HANDLE 
        VIEW-AS ALERT-BOX. 
    DATASET dsOrder:FILL(). 
 END PROCEDURE. 

Run refCaller.p again and you can see the proof. When refCallee.p is first run, it gets a handle for its own ProDataSet:

Next, the calling procedure displays the handle of its copy of the ProDataSet:

Now it runs fillProc:

You can see that fillProc’s ProDataSet has the same handle as the one in the calling procedure refCaller.p. In fact, it is the same ProDataSet, the one with no Data-Sources.

If you change the persistent procedure to do all its work in the internal procedure, then everything works, as shown:

/* refCallee.p */ 
 {dsOrderTT.i} 
 {dsOrder.i} 
 DEFINE DATA-SOURCE srcOrder FOR Customer. 
 DEFINE DATA-SOURCE srcOline FOR OrderLine. 
 DEFINE DATA-SOURCE srcItem FOR ITEM. 
    
 PROCEDURE fillProc: 
    DEFINE OUTPUT PARAMETER DATASET FOR dsOrder. 
    DEFINE VARIABLE hDset AS HANDLE     NO-UNDO. 
    hDset = DATASET dsorder:HANDLE. 
    hDset:GET-BUFFER-HANDLE(1):ATTACH-DATA-SOURCE( 
                                              DATA-SOURCE srcOrder:HANDLE). 
    hDset:GET-BUFFER-HANDLE(2):ATTACH-DATA-SOURCE( 
                                              DATA-SOURCE srcOline:HANDLE). 
    hDset:GET-BUFFER-HANDLE(3):ATTACH-DATA-SOURCE( 
                                              DATA-SOURCE srcItem:HANDLE). 
    DATASET dsOrder:FILL(). 
 END PROCEDURE. 

Figure 2–1 shows what’s happening.

Figure 2–1: Passing ProDataSets

Procedure refCallee.p has a definition of dsOrder, but the ProDataSet instance this represents is replaced by the one from refCaller.p when its ProDataSet is passed BY-REFERENCE. All internal references to hDset are therefore invalid because they point to a ProDataSet instance that isn’t being used. This teaches two important lessons, as described in the following design tips.

Design tip: Don’t set ProDataSet handles at the main procedure level when they will be accessed in internal procedures. Set the handle variables where they are used to capture a reference to an externally defined ProDataSet.

Design tip: It’s always good practice to perform operations such as attaching Data-Sources locally to where they are needed. It’s essential if the ProDataSet is being passed by reference.

Note: These procedures use the standard include files for the temp-table and ProDataSet definitions. Adding the REFERENCE-ONLY keyword to these definitions would improve the performance of these procedures by avoiding the instantiation of the called routine’s objects. It would also avoid the run-time errors by telling Progress at compile time that the called procedure's ProDataSet is not actually being used.

Specifying BY-VALUE in the called procedure

The parameter list shared by the calling procedure and the called procedure represents a contract between the two procedures that defines how they exchange data. As the cases we explored above illustrate, passing a ProDataSet BY-REFERENCE is a valuable optimization but one with side effects that change the nature of the contract between caller and callee. In some cases, the called procedure might want to force a ProDataSet parameter to be passed by value, regardless of any optimization used by the caller, to enforce the contract of its parameter list, and to avoid some of the side effects that can occur. For example, the called procedure might have some reason why it has to reference the ProDataSet handle in its main block and have that handle retain its validity inside internal procedures. Or, it might need to insist that an INPUT parameter should not result in the caller being able to see changes made to the ProDataSet in the called procedure. In any such case, the called procedure can include the BY-VALUE keyword in its parameter definition to force the ProDataSet to be passed by value, regardless of the caller, as shown:

DEFINE [ INPUT | OUTPUT | INPUT-OUTPUT ] PARAMETER DATASET FOR dataset-name 
   BY-VALUE. 
Or 
DEFINE [INPUT | OUTPUT | INPUT-OUTPUT] PARAMETER DATASET-HANDLE dataset-handle 
   BY-VALUE. 

Importance of optimized code with BY-REFERENCE
Note: The information in this section applies to the use of BIND as well, which you will read about in the "Passing a ProDataSet parameter by binding" section.

This diversion into a discussion of BY-REFERENCE might seem overly complex, but it has been introduced here for a reason. If you get into the habit of structuring your procedures and their ProDataSet parameters properly from the beginning, you will be well positioned to optimize most calls that are sometimes made locally and sometimes remotely by adding the BY-REFERENCE keyword to your calls without any undesirable consequences. It is always best to design your procedures so that they work properly when run locally—even when in a deployed application they may be distributed on different machines. This makes the initial development and testing of your application more straightforward, and supports the case (even if it’s only for demo purposes) where everything is running in a single session. Since the BY-REFERENCE optimization is such a valuable one, it is worth making the effort to prepare for it right from the start, even if you first code and test your application without it and then add the keyword to your calls to improve performance.

You can pass a ProDataSet reference locally as a HANDLE PARAMETER as you can with other objects such as temp-tables. This gives the called procedure access to the ProDataSet instance defined in the caller, but has two essential limitations. First, the called procedure must be in the same Progress session as the calling procedure. You can’t pass an object handle of any kind across an AppServer call and have it maintain its validity. Second, the called procedure can only reference the ProDataSet using dynamic attributes and methods if it receives it as a HANDLE. In other words, the called procedure can’t receive the HANDLE parameter into a static DATASET definition and reference it using static table and field names.

By contrast, when you pass or receive a ProDataSet as a DATASET-HANDLE, this simply means that the procedure using the DATASET-HANDLE parameter form sees the ProDataSet as a dynamic object. The procedure on the other side of the call can see it as a static object. The table below shows the different combinations. Normally, passing a ProDataSet as a DATASET-HANDLE causes its definition and data to be copied to the receiving procedure. Using the BY-REFERENCE option on the parameter makes passing a ProDataSet as a DATASET-HANDLE cost no more within a single session than passing a reference to it as a HANDLE. For this reason, we recommend that you normally use the DATASET-HANDLE form. In this way, if the call is ever moved to a remote procedure, it will still work properly, whereas the HANDLE parameter will fail on a remote call.

Passing a ProDataSet BY-REFERENCE is particularly valuable when the called procedure uses the DATASET parameter form to receive the ProDataSet into a static definition. This will be the norm in most server-side procedures, and especially in most event handling procedures. The business logic in these procedures is much simpler to write if it uses static 4GL statements to refer to and manipulate the records in the ProDataSet’s temp-tables. All the elements of the ProDataSet definition, along with all the data in its temp-tables, are available to the called procedure. The default buffers that are part of the ProDataSet, which have the same names as their temp-tables, are in fact shared between caller and callee. Therefore, if the called procedure changes the record position in any of these buffers, this will be visible to the caller after the procedure returns. The called procedure can define buffers of its own to avoid this. Any changes to the ProDataSet data made by the called procedure are visible to the caller, as if the object were SHARED.

There are several restrictions in ProDataSet usage that are at least partially related to this parameter support:

As a final note, it is possible to specify a ProDataSet as a parameter for a dynamic CALL object, which lets you set up the entire definition of a RUN statement dynamically. You can also specify the BY-REFERENCE qualifier in the arguments to the dynamic CALL. For more information, see the reference documentation or online help for the SET-PARAMETER( ) method on the dynamic CALL object.


Copyright © 2005 Progress Software Corporation
www.progress.com
Voice: (781) 280-4000
Fax: (781) 280-4095